package com.jintian.smart.kernel.switching.runtime.db;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.jintian.smart.kernel.orm.dto.Cascade;
import com.jintian.smart.kernel.switching.api.IFileAdapter;
import com.jintian.smart.kernel.switching.api.ILoader;
import com.jintian.smart.kernel.switching.db.ExtractFileAdapter;
import com.jintian.smart.kernel.switching.db.ExtractRule;
import com.jintian.smart.kernel.switching.db.ExtractTable;
import com.jintian.smart.kernel.switching.dto.db.Attachment;
import com.jintian.smart.kernel.switching.dto.db.DBDataPacket;
import lombok.extern.slf4j.Slf4j;
import net.lingala.zip4j.ZipFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput;
import org.beetl.sql.annotation.entity.Version;
import org.beetl.sql.core.ExecuteContext;
import org.beetl.sql.core.SQLManager;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;

import java.io.*;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Map.Entry;

@SuppressWarnings("rawtypes")
@Slf4j
public class SimpleDBLoader implements ILoader, ApplicationContextAware {
	private final SQLManager sqlManager;

	private final ObjectMapper objectMapper;

	@Value("${mda.switching.rule-path}")
	private String rulePath;

	@Autowired
	private ApplicationContext applicationContext;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public ApplicationContext getApplicationContext() {
		return this.applicationContext;
	}

	private IFileAdapter getFileAdapter(Class storageBeanClass) {
		ApplicationContext app = this.applicationContext;
		while (app != null) {
			Collection<IFileAdapter> list = app.getBeansOfType(IFileAdapter.class).values();
			for (IFileAdapter IFileAdapter : list) {
				if (IFileAdapter.support(storageBeanClass)) {
					return IFileAdapter;
				}
			}
			app = app.getParent();
		}

		throw new IllegalArgumentException("未注册支持" + storageBeanClass + "的FileAdapter");
	}

	public SimpleDBLoader(SQLManager sqlManager) {
		this.sqlManager = sqlManager;

		JavaTimeModule module = new JavaTimeModule();
		LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(
				DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
		LocalDateTimeSerializer dateTimeSerializer = new LocalDateTimeSerializer(
				DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
		module.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
		module.addSerializer(LocalDateTime.class, dateTimeSerializer);
		this.objectMapper = Jackson2ObjectMapperBuilder.json()
				.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).locale(Locale.CHINA)
				.timeZone(TimeZone.getTimeZone("GMT+8")).modules(module).build();
	}

	private ClassLoader getClassLoader() {
		return this.applicationContext.getClassLoader();
	}

	@Override
	public boolean support(Class<?> dataType, Class<?> ruleClass, String ruleName) {
		return DBDataPacket.class.equals(dataType) && ExtractRule.class.equals(ruleClass) && this.applicationContext
				.getResource("classpath:etl/" + this.rulePath + "/" + ruleName + ".xml").exists();
	}

	@Override
	public Object load(File dataFile) {
		File tempDir = new File(FileUtils.getTempDirectory(), UUID.randomUUID().toString());
		ClassLoader old = Thread.currentThread().getContextClassLoader();
		try {
			boolean b = false;
			// 解压数据
			try (ZipFile zip = new ZipFile(dataFile)) {
				zip.extractAll(tempDir.getPath());
				b = true;
			} catch (Exception e) {
				if (!b) {
					throw new RuntimeException("解压数据失败", e);
				}
			}
			return this.loadDataDir(tempDir);
		} catch (Exception e) {
			if (e instanceof RuntimeException) {
				throw (RuntimeException) e;
			}
			throw new RuntimeException(e);
		} finally {
			try {
				FileUtils.deleteDirectory(tempDir);
			} catch (IOException e) {
				e.printStackTrace();
			}
			Thread.currentThread().setContextClassLoader(old);
		}
	}

	private DBDataPacket loadDataDir(File dataDir) throws IOException {
		// 忽略UploadValue注解
		ClassLoader old = Thread.currentThread().getContextClassLoader();
		ExecuteContext.disableUpdateValue(true);
		try (FileInputStream in = new FileInputStream(new File(dataDir, SimpleDBExtractor.FILE_DATA_DUMP))) {
			ClassLoader clsLoader = this.getClassLoader();
			Thread.currentThread().setContextClassLoader(clsLoader);
			Hessian2ObjectInput input = new Hessian2ObjectInput(in);
			DBDataPacket dataPacket = (DBDataPacket) input.readObject();

			for (ExtractTable table : dataPacket.getRule().getTables()) {
				this.cascadeTableData(null, table, dataPacket, clsLoader);
				this.upsertTableData(table, dataPacket, clsLoader);
			}
			File attachmentDir = new File(dataDir, SimpleDBExtractor.DIR_ATTACHMENT);
			// 上传文件
			this.uplaodAttachemnt(attachmentDir, clsLoader, dataPacket);
			return dataPacket;
		} finally {
			Thread.currentThread().setContextClassLoader(old);
			ExecuteContext.disableUpdateValue(false);
		}
	}

	@Override
	public Object load(byte[] data) {
		File dataFile = new File(FileUtils.getTempDirectory(), UUID.randomUUID().toString());
		try (OutputStream outputStream = new FileOutputStream(dataFile)) {
			IOUtils.copy(new ByteArrayInputStream(data), outputStream);
			return this.load(dataFile);
		} catch (Exception e) {
			if (e instanceof RuntimeException) {
				throw (RuntimeException) e;
			}
			throw new RuntimeException(e);
		} finally {
			dataFile.delete();
		}
	}

	@SuppressWarnings("unchecked")
	private void upsertTableData(ExtractTable table, DBDataPacket dataPacket, ClassLoader clsLoader) {
		List list = dataPacket.getUpsertData(table.getName());
		if (list != null) {
			// 先更新主表
			if (log.isDebugEnabled()) {
				log.debug("upsert {}", table.getName());
			}
			Class cls = this.loadClass(clsLoader, table.getClazz());
			list.forEach(obj -> this.upsertBean(obj, cls));
			// 再更新明细
			for (ExtractTable subTable : table.getChildren()) {
				this.upsertTableData(subTable, dataPacket, clsLoader);
			}
		}
	}

	private boolean isFileStorageTable(DBDataPacket dataPacket, String tableName) {
		List<ExtractFileAdapter> list = dataPacket.getRule().getFileAdapters();
		for (ExtractFileAdapter fileAdapter : list) {
			List<ExtractFileAdapter.RefTable> refs = fileAdapter.getRefTables();
			for (ExtractFileAdapter.RefTable ref : refs) {
				if (ref.getRef().equals(tableName)) {
					return true;
				}
			}
		}
		return false;
	}

	@SuppressWarnings("unchecked")
	private void cascadeTableData(ExtractTable parent, ExtractTable table, DBDataPacket dataPacket,
			ClassLoader clsLoader) {
		// 执行删除keys
		Class clazz = this.loadClass(clsLoader, table.getClazz());
		// 先级联子表
		for (ExtractTable subTable : table.getChildren()) {
			this.cascadeTableData(table, subTable, dataPacket, clsLoader);
		}
		boolean isFileTable = this.isFileStorageTable(dataPacket, table.getName());
		// 级联删除或更新
		if (parent != null && table.getCascade() != null && !ExtractTable.Cascade.NONE.equals(table.getCascade())) {
			String idAttr = this.sqlManager.getClassDesc(this.loadClass(clsLoader, parent.getClazz())).getIdAttr();
			String[] foreignKeys = table.getForeignKey().split(";");
			StringBuffer where = new StringBuffer();
			StringBuffer set = new StringBuffer();
			for (String foreignKey : foreignKeys) {
				String[] args = foreignKey.split("=");
				String subField = this.sqlManager.getNc().getColName(clazz, args[0].trim());
				String parentAttr = args.length == 1 ? idAttr : args[1].trim();
				//
				set.append(",").append(subField).append("=null");
				//
				where.append(" and ").append(subField).append("=");
				where.append("#{").append(parentAttr).append("}");
			}
			String tableName = this.sqlManager.getNc().getTableName(clazz);
			List parentDataList = dataPacket.getUpsertData(parent.getName());
			for (Object parentRow : parentDataList) {
				Map dataMap = this.objectMapper.convertValue(parentRow, HashMap.class);
				if (Cascade.DELETE.equals(table.getCascade())) {
					if (isFileTable) {
						IFileAdapter IFileAdapter = this.getFileAdapter(table.getClass());
						String sql = "select * from " + this.sqlManager.getNc().getTableName(clazz) + " where "
								+ where.toString().substring(5);
						List allData = this.sqlManager.execute(sql, clazz, dataMap);
						for (Object fileStorage : allData) {
							IFileAdapter.delete(fileStorage);
						}
					}
					String sql = "delete from " + tableName + " where " + where.toString().substring(5);
					this.sqlManager.executeUpdate(sql, dataMap);
				} else if (Cascade.SETNULL.equals(table.getCascade())) {
					String sql = "update " + tableName + " set " + set.toString().substring(1) + " where "
							+ where.toString().substring(5);
					this.sqlManager.executeUpdate(sql, dataMap);
				}
			}
		}
		// 同步删除日志
		List<String> keys = dataPacket.getDeleteData(table.getName());
		if (keys != null) {
			if (isFileTable) {
				IFileAdapter IFileAdapter = this.getFileAdapter(table.getClass());
				List allData = this.sqlManager.selectByIds(clazz, keys);
				for (Object fileStorage : allData) {
					IFileAdapter.delete(fileStorage);
				}
			}
			this.sqlManager.deleteByIds(clazz, keys);
		}

	}

	private void uplaodAttachemnt(File attachmentDir, ClassLoader clsLoader, DBDataPacket dataPacket) {
		for (Entry<String, List<Attachment>> e : dataPacket.getAttachments().entrySet()) {
			String refName = e.getKey();
			ExtractTable table = this.getExtractTableByName(dataPacket.getRule().getTables(), refName);
			if (table == null)
				throw new IllegalArgumentException("不存在ExtractTable[" + refName + "]");
			List refTableData = dataPacket.getUpsertData(refName);
			IFileAdapter IFileAdapter = this.getFileAdapter(refTableData.get(0).getClass());
			List<Attachment> refTableAttachments = e.getValue();
			Class cls = this.loadClass(clsLoader, table.getClazz());
			for (Attachment attachment : refTableAttachments) {
				// 获取附件Bean
				Object attachmentBean = this.getAttachmentBean(refTableData, IFileAdapter, attachment.getKey());
				// 上传并更新存储路径
				IFileAdapter.upload(attachmentBean, attachment, attachmentDir);
				this.upsertBean(attachmentBean, cls);
			}
		}
	}

	private Class loadClass(ClassLoader clsLoader, String clazz) {
		try {
			return clsLoader.loadClass(clazz);
		} catch (Exception e) {
			throw new IllegalArgumentException(e.getMessage());
		}
	}

	private void upsertBean(Object bean, Class cls) {
		@SuppressWarnings("unchecked")
		Object old = this.sqlManager.single(cls, bean);
		if (old == null) {
			sqlManager.upsert(bean);
		} else {
			List<String> ignoreProperties = new ArrayList<>();
			ReflectionUtils.doWithFields(cls, new FieldCallback() {
				@Override
				public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
					ignoreProperties.add(field.getName());
				}
			}, new FieldFilter() {
				@Override
				public boolean matches(Field field) {
					return field.getAnnotation(Version.class) != null;
				}
			});
			BeanUtils.copyProperties(bean, old, ignoreProperties.toArray(new String[0]));
			sqlManager.upsert(old);
		}
	}

	private ExtractTable getExtractTableByName(List<ExtractTable> list, String name) {
		for (ExtractTable table : list) {
			if (table.getName().equals(name)) {
				return table;
			}
			ExtractTable t = getExtractTableByName(table.getChildren(), name);
			if (t != null)
				return t;
		}
		return null;
	}

	private Object getAttachmentBean(List list, IFileAdapter IFileAdapter, String key) {
		for (Object bean : list) {
			if (IFileAdapter.getBeanKey(bean).equals(key)) {
				return bean;
			}
		}
		throw new IllegalArgumentException("不存在id为[" + key + "]附件数据对象");
	}

}
